#include <cstdint>
#include <cstring>

#ifdef _MSC_VER
#include <BaseTsd.h>
using ssize_t = SSIZE_T;
#endif

#include <fcntl.h>

#include "spdlog/spdlog.h"

#include "SocketReaderDetail.hpp"

#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif

namespace aeye {

namespace detail {

// wrap conditionally compiled code
inline int close(int fd) noexcept {
#ifdef _MSC_VER
  return ::closesocket(fd);
#else
  return ::close(fd);
#endif
}

} // end namespace detail

Reader::Reader(const std::string &sensorAddr, uint16_t clientPort, uint16_t serverPort, const bool broadcastMode)
    : broadcast_mode_(broadcastMode) {

    if(sensorAddr == "127.0.0.1") {
        serverPort = static_cast<uint16_t>(DEFAULT_SERVER_POINTS_PORT);
    }
  local_addr_int_.sin_family = AF_INET;
  local_addr_int_.sin_port = htons(clientPort); // NOLINT
  local_addr_int_.sin_addr.s_addr = htonl(INADDR_ANY);

  sensor_addr_int_.sin_family = AF_INET;
  sensor_addr_int_.sin_port = htons(serverPort); // NOLINT
  sensor_addr_int_.sin_addr.s_addr = inet_addr(sensorAddr.c_str());
}

int Reader::setNonblockingAttribute() const noexcept {
  int flags = 0;
//   If we have O_NONBLOCK, use the Posix way to do it
#if defined(O_NONBLOCK)
  //  Fixme: O_NONBLOCK is defined but broken on SunOS 4.1.x and AIX 3.2.5.
  flags = fcntl(sockfd_, F_GETFL, 0);
  if (flags == -1)
    return -1;

  // flags &= (~O_NONBLOCK);  // for blocking
  // flags |= O_NONBLOCK;     // for non-blocking
  return fcntl(sockfd_, F_SETFL, flags | O_NONBLOCK);
#else
  //   Otherwise, use the old way of doing it
    flags = 1;
#ifdef _MSC_VER
    return ioctlsocket(sockfd_, FIONBIO, (u_long*) &flags);
#else
    return ioctl(sockfd_, FIOBIO, &flags);
#endif
#endif
}

int Reader::getSO_ERROR() const noexcept {
  int err = 1;
  socklen_t len = sizeof(err);
  if (-1 == getsockopt(sockfd_, SOL_SOCKET, SO_ERROR, reinterpret_cast<char *>(&err), &len)) {
    spdlog::warn("UDP getsockopt reports `{1}`, errno {0}.", errno, strerror(errno));
    return -1;
  }
  if (err) errno = err;              // set errno to the socket SO_ERROR
  return err;
}

bool Reader::validate_ip_addr(const sockaddr_in &src_addr) const noexcept {
  if (!broadcast_mode_ && src_addr.sin_addr.s_addr != sensor_addr_int_.sin_addr.s_addr) {
    // only perform ntop conversion to report invalid ip addresses
    char source_addr[INET_ADDRSTRLEN]{};
    inet_ntop(AF_INET, &(src_addr.sin_addr), source_addr, INET_ADDRSTRLEN);
    spdlog::warn("UDP recvfrom reports source IP \"{}\" not valid.", source_addr);
    return false;
  }
  return true;
}

void Reader::start() {
#ifdef _MSC_VER
  BOOL opt_on = true;
#else
  int opt_on = 1;
#endif

  if (sockfd_ >= 0) {
    spdlog::info("Socket already open, leaving it alone.");
    return;
  }

  sockfd_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (sockfd_ < 0) {
    spdlog::error("UDP socket ctor reports `{1}`, errno {0}.", errno, strerror(errno));
    throw aeye::socket_error();
  }
#ifdef _MSC_VER
  //WinSock2.h setsockopt documentation
  //setsockopt(_In_ SOCKET s,_In_ int level,_In_ int optname,_In_reads_bytes_opt_(optlen) const char FAR* optval,_In_ int optlen);
  if (setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, (char*)&opt_on, sizeof(opt_on)) != 0) {
    spdlog::error("setsockopt reports `{1}`, errno {0}.", errno, strerror(errno));
    throw aeye::socket_error();
 }
#elif __APPLE__
  if (setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt_on, sizeof(opt_on)) < 0) {
    spdlog::error("setsockopt SO_REUSEADDR reports `{1}`, errno {0}.", errno, strerror(errno));
    throw aeye::socket_error();
  }
    if (setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &opt_on, sizeof(opt_on)) < 0) {
    spdlog::error("setsockopt SO_REUSEPORT reports `{1}`, errno {0}.", errno, strerror(errno));
    throw aeye::socket_error();
  }
#else
  //sys/socket.h setsockopt documentation
  //int setsockopt(int socket, int level, int option_name, const void* option_value, socklen_t option_len);
  const auto options = SO_REUSEADDR | SO_REUSEPORT | (broadcast_mode_ ? SO_BROADCAST : 0);
  if (setsockopt(sockfd_, SOL_SOCKET, options, &opt_on, sizeof(opt_on)) < 0) {
    spdlog::error("setsockopt reports `{1}`, errno {0}.", errno, strerror(errno));
    throw aeye::socket_error();
  }
#endif
  if (bind(sockfd_, reinterpret_cast<struct sockaddr *>(&local_addr_int_), sizeof(local_addr_int_))) {
    spdlog::error("bind reports `{1}`, errno {0}.", errno, strerror(errno));
    throw aeye::socket_error();
  }
  if (!broadcast_mode_){
    if (connect(sockfd_, reinterpret_cast<struct sockaddr *>(&sensor_addr_int_), sizeof(sensor_addr_int_)) < 0) {
     spdlog::error("connect reports `{1}`, errno {0}.", errno, strerror(errno));
     throw aeye::socket_error();
    }
  }

  setNonblockingAttribute();
}

void Reader::stop() noexcept {
  if (sockfd_ < 0) return;

  getSO_ERROR(); // first clear any errors, which can cause close to fail

  if (shutdown(sockfd_, 2) < 0) {
    if (errno != ENOTCONN && errno != EINVAL) {// SGI causes EINVAL
      spdlog::warn("UDP shutdown reports `{1}`, errno {0}.", errno, strerror(errno));
      getSO_ERROR(); // clear any errors, which can cause close to fail
    }
  }
  if (detail::close(sockfd_) < 0) { // finally call close()
    spdlog::warn("UDP close reports `{1}`, errno {0}.", errno, strerror(errno));
    if (errno == EINTR) { // The close() function was interrupted by a signal.
      // Try again
      if (detail::close(sockfd_) < 0 && errno == EINTR) {
        spdlog::warn("UDP close (attempt 2) reports `{1}`, errno {0}.", errno, strerror(errno));
      }
      sockfd_ = -1;
    }
  }
  sockfd_ = -1;
}

bool Reader::run_select(unsigned int s, unsigned int us, fd_set &readFlags) noexcept {
  FD_ZERO(&readFlags);
  FD_SET(sockfd_, &readFlags); // NOLINT (that's just how you do it, sorry clang-tidy)
  // need to reinit timeval every call to select() to not burn massive amounts of cpu time
#ifdef __APPLE__
  struct timeval tv{};
  tv.tv_sec = s;
  tv.tv_usec = static_cast<__darwin_suseconds_t>(us);
#else
  struct timeval tv { s, us };
#endif
  int selectReturn = select(sockfd_ + 1, &readFlags, nullptr, nullptr, &tv);
  if (selectReturn == -1) {
    // error happened
    spdlog::warn("UDP select reports `{1}`, errno {0}.", errno, strerror(errno));
    return false;
  }
  else if (selectReturn == 0) {
    // udp socket timeout
    return false;
  }
  return true;
}

int Reader::is_set(const fd_set &readFlags) const noexcept {
  // Prevents an odd buffer overflow from occuring on certain computers
  if (sockfd_ < 0) return 0;

  return FD_ISSET(sockfd_, &readFlags); // NOLINT (that's just how you do it, sorry clang-tidy)
}

size_t Reader::receive_bytes(uint8_t *buf, sockaddr_in &src_addr) const noexcept {
  socklen_t socklen = sizeof(src_addr);
  // ready to read
  // posix says recvfrom will always return entire message for DGRAM,
  //   linux man page does not. Looks like we are assuming posix compliance here. Should verify ?
#ifdef _MSC_VER
  const ssize_t bytesReceived = recvfrom(sockfd_, reinterpret_cast<char*>(buf), UDP_BUF_LEN, MSG_NOSIGNAL, (struct sockaddr *)&src_addr, &socklen); // receive normal data
#else
  const ssize_t bytesReceived = recvfrom(sockfd_, buf, UDP_BUF_LEN, MSG_NOSIGNAL, reinterpret_cast<struct sockaddr*>(&src_addr), &socklen);
#endif
  if (bytesReceived < 0) {
    if (errno != EAGAIN) {
      spdlog::warn("UDP recvfrom reports `{1}`, errno {0}.", errno, strerror(errno));
    }
    return 0;
  } else if (bytesReceived == 0) {
    // connection was closed by the server
    spdlog::info("bytesReceived == 0. Connection was closed by the server.");
    return 0;
  }
  return static_cast<size_t>(bytesReceived);
}
bool Reader::find_marker(const char (&arr)[4]) noexcept {
  static constexpr const char marker[4] = {'A', 'E', 'Y', 'E'};
  // memcmp is more efficient than str(n)cmp for non-null terminated strings
  return 0 == std::memcmp(arr, marker, 4);
}

}